package com.upb.webserver.common.tools.encryption;

import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import org.bouncycastle.jce.spec.ECPublicKeySpec;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemWriter;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;


/**
 *
 */

@Slf4j
public class EccKeyUtil {

    public static final String SM2_CURVE_NAME = "sm2p256v1";
    public static final String ECDSA = "ECDSA";

    public static final String BC = BouncyCastleProvider.PROVIDER_NAME;
    public static final ECNamedCurveParameterSpec PARAMETER_SPEC;
    public static final ECNamedCurveSpec SPEC;
    private static KeyFactory keyFactory;

    static {
        Security.setProperty("crypto.policy", "unlimited");

        Security.addProvider(new BouncyCastleProvider());

        PARAMETER_SPEC = ECNamedCurveTable.getParameterSpec(SM2_CURVE_NAME);
        SPEC = new ECNamedCurveSpec(SM2_CURVE_NAME, PARAMETER_SPEC.getCurve(),
                PARAMETER_SPEC.getG(), PARAMETER_SPEC.getN(),
                PARAMETER_SPEC.getH(), PARAMETER_SPEC.getSeed());

        try {
            keyFactory = KeyFactory.getInstance(ECDSA, BouncyCastleProvider.PROVIDER_NAME);
        } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
            log.error("Init ECDSA failed.", e);
            System.exit(1);
        }
    }


    /**
     * Convert  private key from format {@link BigInteger} to {@link PrivateKey}
     *
     * @param privateKey private key in {@link BigInteger}.
     * @return private key in {@link PrivateKey}.
     * @throws InvalidKeySpecException {@link InvalidKeySpecException} throws if convert error.
     */
    public static PrivateKey getPrivateKey(BigInteger privateKey) throws InvalidKeySpecException {
        ECPrivateKeySpec keySpec = new ECPrivateKeySpec(privateKey, SPEC);
        return keyFactory.generatePrivate(keySpec);
    }

    /**
     * Get public key in {@link ECPoint} format from private key.
     *
     * @param privateKey private key in {@link BigInteger}.
     * @return public key in {@link ECPoint}
     */
    private static ECPoint getPublicKeyECPoint(BigInteger privateKey) {
        if (privateKey.bitLength() > PARAMETER_SPEC.getN().bitLength()) {
            privateKey = privateKey.mod(PARAMETER_SPEC.getN());
        }
        return (new FixedPointCombMultiplier()).multiply(PARAMETER_SPEC.getG(), privateKey);
    }


    /**
     * Get public key in {@link PublicKey} format from private key.
     *
     * @param privateKey private key in {@link BigInteger}.
     * @return public key in {@link PublicKey}
     * @throws InvalidKeySpecException {@link InvalidKeySpecException} throws if error.
     */
    public static PublicKey getPublicKey(BigInteger privateKey) throws InvalidKeySpecException {
        ECPoint publicKey = getPublicKeyECPoint(privateKey);
        return getPublicKey(publicKey);
    }


    /**
     * Get public key in {@link PublicKey} format from hex of {@link ECPoint}.
     *
     * @param ecPointInHex hex of public key in {@link ECPoint}.
     * @return public key in {@link PublicKey}
     * @throws InvalidKeySpecException {@link InvalidKeySpecException} throws if error.
     */
    public static PublicKey getPublicKeyFromHex(String ecPointInHex) throws InvalidKeySpecException {
        ECPoint ecPoint = PARAMETER_SPEC.getCurve().decodePoint(HexUtil.hexToByteArray(ecPointInHex));
        ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(ecPoint, PARAMETER_SPEC);
        ECPublicKey pk = (ECPublicKey) keyFactory.generatePublic(pubKeySpec);
        return pk;
    }


    /**
     * Get public key in {@link PublicKey} format from {@link ECPoint}.
     *
     * @param ecPoint public key in {@link ECPoint}.
     * @return public key in {@link PublicKey}
     * @throws InvalidKeySpecException {@link InvalidKeySpecException} throws if error.
     */
    public static PublicKey getPublicKey(ECPoint ecPoint) throws InvalidKeySpecException {
        Security.addProvider(new BouncyCastleProvider());
        ECNamedCurveParameterSpec parameterSpec = ECNamedCurveTable.getParameterSpec(SM2_CURVE_NAME);
        ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(ecPoint, parameterSpec);
        ECPublicKey publicKey = (ECPublicKey) keyFactory.generatePublic(ecPublicKeySpec);
        return publicKey;

    }

//    /**
//     * Save private key of {@link BigInteger} into pem file.
//     *
//     * @param filePath   pem file to save private key.
//     * @param privateKey private key of {@link BigInteger}
//     * @throws InvalidKeySpecException {@link InvalidKeySpecException} throws if error.
//     * @throws IOException             {@link IOException} throws if error.
//     */
//    public static void savePrivateToPem(String filePath, BigInteger privateKey) throws InvalidKeySpecException, IOException {
//        saveKeyToPem(filePath, PemType.PRIVATE_KEY, getPrivateKey(privateKey).getEncoded());
//    }

//    /**
//     * Save public key of {@link BigInteger} into pem file.
//     *
//     * @param filePath  pem file to save private key.
//     * @param publicKey private key of {@link ECPoint}
//     * @throws InvalidKeySpecException {@link InvalidKeySpecException} throws if error.
//     * @throws IOException             {@link IOException} throws if error.
//     */
//    public static void savePublicToPem(String filePath, ECPoint publicKey) throws InvalidKeySpecException, IOException {
//        saveKeyToPem(filePath, PemType.PUBLIC_KEY, getPublicKey(publicKey).getEncoded());
//    }

//    /**
//     * Save key of bytes to file.
//     *
//     * @param filePath pem file to save private key.
//     * @param type     {@link PemType} key type.
//     * @param encoded  bytes of key.
//     * @throws IOException {@link IOException} throws if error.
//     */
//    private static void saveKeyToPem(String filePath, PemType type, byte[] encoded) throws IOException {
//        try (
//                StringWriter writer = new StringWriter();
//                PemWriter pemWriter = new PemWriter(writer);
//        ) {
//            pemWriter.writeObject(new PemObject(type.getType(), encoded));
//            pemWriter.flush();
//            pemWriter.close();
//            Path path = Paths.get(filePath);
//            if (Files.notExists(path.getParent())) {
//                Files.createDirectories(path.getParent());
//            }
//            Files.write(Paths.get(filePath), Arrays.asList(writer.toString()));
//        } catch (IOException e) {
//            throw e;
//        }
//    }

//    /**
//     * Load private key from pem file.
//     *
//     * @param filePath file to load.
//     * @return private key in pem file.
//     * @throws IOException {@link IOException} throws if error.
//     */
//    public static PrivateKey getPrivateKey(String filePath) throws Exception {
//        Path path = normalizePath(filePath);
//
//        PrivateKey privateKey = null;
//        byte[] bytes = Files.readAllBytes(path);
//        try (PEMParser pem = new PEMParser(new StringReader(new String(bytes)))) {
//            Object obj = pem.readObject();
//            JcaPEMKeyConverter jcaPEMKeyConverter = new JcaPEMKeyConverter();
//            if (obj instanceof PrivateKeyInfo) {
//                privateKey = jcaPEMKeyConverter.getPrivateKey((PrivateKeyInfo) obj);
//            } else if (obj instanceof PEMKeyPair) {
//                privateKey = jcaPEMKeyConverter.getPrivateKey(((PEMKeyPair) obj).getPrivateKeyInfo());
//            } else if (obj instanceof ASN1ObjectIdentifier) {
//                throw new Exception("No private key found in the provided file");
//            }
//        } catch (Exception e) {
//            throw e;
//        }
//        return privateKey;
//    }

//    /**
//     * Load public key from pem file.
//     *
//     * @param filePath file to load.
//     * @return public key in pem file.
//     * @throws IOException {@link IOException} throws if error.
//     */
//    public static PublicKey getPublicKey(String filePath) throws Exception {
//        Path path = normalizePath(filePath);
//
//        PublicKey publicKey = null;
//        byte[] bytes = Files.readAllBytes(path);
//        try (PEMParser pem = new PEMParser(new StringReader(new String(bytes)))) {
//            Object obj = pem.readObject();
//            JcaPEMKeyConverter jcaPEMKeyConverter = new JcaPEMKeyConverter();
//            if (obj instanceof SubjectPublicKeyInfo) {
//                publicKey = jcaPEMKeyConverter.getPublicKey((SubjectPublicKeyInfo) obj);
//            } else if (obj instanceof PEMKeyPair) {
//                publicKey = jcaPEMKeyConverter.getPublicKey(((PEMKeyPair) obj).getPublicKeyInfo());
//            } else {
//                throw new Exception("No private key found in the provided file");
//            }
//        } catch (Exception e) {
//            throw e;
//        }
//        return publicKey;
//    }

//    /**
//     * @param filePath
//     * @return
//     */
//    private static Path normalizePath(String filePath) {
//        Path path = Paths.get(filePath).normalize();
//        if (path.toString().contains("..")) {
//            throw new WebServerException(ConstantCode.PARAM_EXCEPTION.msg("Path should not contains [..] ."));
//        }
//        return path;
//    }

//    /**
//     * Load public key from pem certification file.
//     *
//     * @param pemPath file to load.
//     * @return Public key of {@link PublicKey}
//     * @throws IOException             {@link IOException} throws if error.
//     * @throws CertificateException    {@link CertificateException} throws if error.
//     * @throws NoSuchProviderException {@link NoSuchProviderException} throws if error.
//     */
//    public static PublicKey getPublicKeyFromPemCert(String pemPath) throws IOException, CertificateException, NoSuchProviderException {
//        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509", BC);
//        try (FileInputStream fileInputStream = new FileInputStream(pemPath)) {
//            X509Certificate x509Certificate = (X509Certificate) certificateFactory.generateCertificate(fileInputStream);
//            return x509Certificate.getPublicKey();
//        }
//    }
}

